#define FILE_ID dokan

#define _WIN32_WINNT 0x601
#include "fs_win.h"
#include "utils.h"
#include "provider_api.h"
#include "log.h"

#include "winnt.h"

#pragma comment(lib, "dokan.lib")


int ErrToWin32Dokan(int err)
{
	switch(err) {
	case FS_NO_ERROR: return ERROR_SUCCESS;
	case FS_ERROR_ACCESS_DENIED: return -ERROR_ACCESS_DENIED;
	case FS_ERROR_NO_FILE: return -ERROR_FILE_NOT_FOUND;
	case FS_ERROR_GENERIC_ERROR: return -ERROR_GEN_FAILURE;
	case FS_ERROR_FS_DOWN: return -ERROR_FILE_OFFLINE;
	case FS_ERROR_ALREADY_EXISTS: return -ERROR_ALREADY_EXISTS;
	case FS_ERROR_DIR_NOT_EMPTY: return -ERROR_DIR_NOT_EMPTY;
	}
	return -ERROR_ACCESS_DENIED;
}

static std::string ConvertFilename(const LPCWSTR path)
{
	std::string str = Utf16To8(path);
	for(size_t i = 0; i < str.size(); i++)
		if(str[i] == '\\')
			str[i] = '/';
	return str;
}

static int DOKAN_CALLBACK DokanCreateFile(
	LPCWSTR       FileName,
	DWORD         DesiredAccess,
	DWORD         ShareMode,
	DWORD         CreationDisposition,
	DWORD         FlagsAndAttributes,
	PDOKAN_FILE_INFO info)
{
	std::string name = ConvertFilename(FileName);
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	if(name.size() == 1 && name[0] == '/') {
		return fs->OpenRootDir(info);
	}

	LOG(LogVerbose, "CreateFile %s", name.c_str());
	int ret = fs->CreateFile(name, DesiredAccess, ShareMode, CreationDisposition, FlagsAndAttributes, info);
	LOG(LogInfo, "CreateFile %s %p by %d - ret %d", name.c_str(), (void*)info->Context, info->ProcessId, ret);
	return ret;
}

static int DOKAN_CALLBACK DokanOpenDirectory(
	LPCWSTR				 FileName,
	PDOKAN_FILE_INFO info)
{
	std::string name = ConvertFilename(FileName);
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	if(name.size() == 1 && name[0] == '/') {
		return fs->OpenRootDir(info);
	}
	LOG(LogVerbose, "OpenDirectory %s", name.c_str());
	int ret = fs->OpenDirectory(name, info);
	LOG(LogInfo, "OpenDirectory %s %p ret %d", name.c_str(), (void*)info->Context, ret);
	return ret;
}

static int DOKAN_CALLBACK DokanCreateDirectory(
	LPCWSTR				 FileName,
	PDOKAN_FILE_INFO info)
{
	std::string name = ConvertFilename(FileName);
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	LOG(LogVerbose, "CreateDirectory %s", name.c_str());
	int ret = fs->CreateDirectory(name, info);
	LOG(LogInfo, "CreateDirectory %s %p ret %d", name.c_str(), (void*)info->Context, ret);
	return ret;
}

// When FileInfo->DeleteOnClose is true you must delete the file in Cleanup.
static int DOKAN_CALLBACK DokanCleanup(
	LPCWSTR       FileName,
	PDOKAN_FILE_INFO info)
{
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	int ret = fs->Cleanup(info);
	return ret;
}

static int DOKAN_CALLBACK DokanCloseFile(
	LPCWSTR       FileName,
	PDOKAN_FILE_INFO info)
{
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	int ret = fs->Close(info);
	return ret;
}

static int DOKAN_CALLBACK DokanReadFile(
	LPCWSTR   FileName,
	LPVOID    Buffer,
	DWORD     NumberOfBytesToRead,
	LPDWORD   NumberOfBytesRead,
	LONGLONG  Offset,
	PDOKAN_FILE_INFO info)
{
	if(info->IsDirectory) {
		LOG(LogWarning, "IsDirectory on ReadFile");
		return -ERROR_INVALID_HANDLE;
	}
	ProviderFile *file = GetFileObj(info);
	if(!file) {
		LOG(LogWarning, "NULL file on ReadFile");
		return -ERROR_INVALID_HANDLE;
	}

	unsigned long n;
	LOG(LogVerbose, "Read %p %d bytes @ %I64u", (void*)info->Context, NumberOfBytesToRead, Offset);
	int ret = file->Read(Buffer, Offset, NumberOfBytesToRead, n);
	LOG(ret ? LogError : LogInfo, "Read %p %d bytes @ %I64u, got %d, ret %d", (void*)info->Context, NumberOfBytesToRead, Offset, n, ret);
	*NumberOfBytesRead = n;
	return ErrToWin32Dokan(ret);
}


static int DOKAN_CALLBACK DokanWriteFile(
	LPCWSTR   FileName,
	LPCVOID   Buffer,
	DWORD     NumberOfBytesToWrite,
	LPDWORD   NumberOfBytesWritten,
	LONGLONG  Offset,
	PDOKAN_FILE_INFO info)
{
	ProviderFile *file = GetFileObj(info);
	if(!file)
		return -ERROR_INVALID_HANDLE;
	unsigned long n;
	int ret = file->Write((void*)Buffer, Offset, NumberOfBytesToWrite, n);
	*NumberOfBytesWritten = n;
	return ErrToWin32Dokan(ret);
}


static int DOKAN_CALLBACK DokanFlushFileBuffers(
	LPCWSTR  FileName,
	PDOKAN_FILE_INFO info)
{
	ProviderFile *file = GetFileObj(info);
	if(!file)
		return -ERROR_INVALID_HANDLE;
	LOG(LogVerbose, "FlushFileBuffers %p", (void*)info->Context);
	return ErrToWin32Dokan(file->Flush());
}


static int DOKAN_CALLBACK DokanGetFileInformation(
	LPCWSTR           FileName,
	LPBY_HANDLE_FILE_INFORMATION  Buffer,
	PDOKAN_FILE_INFO info)
{
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	LOG(LogVerbose, "GetFileInfo %p", (void*)info->Context);
	int ret = fs->GetFileInformation(Buffer, info);
	LOG(LogVerbose, "GetFileInfo %p ret %d", (void*)info->Context, ret);
	return ret;
}

struct FindInfo
{
	PFillFindData Func;
	PDOKAN_FILE_INFO Info;
};

static void FindFunc(const char *name, size_t namelen, const DirStatInfo& info, void *context)
{
	FindInfo *arg = (FindInfo*)context;
	WIN32_FIND_DATAW find;
	memset(&find, 0, sizeof(find));
	std::string str;
	str.assign(name, namelen);
	std::wstring wstr = Utf8To16(str);
	size_t sz = wstr.size() * 2;
	memcpy(find.cFileName, wstr.c_str(), min(sz, sizeof(find.cFileName)-2));
	find.nFileSizeHigh = (DWORD)(info.FileSize >> 32);
	find.nFileSizeLow = (DWORD)(info.FileSize & 0xFFFFFFFF);
	find.ftLastWriteTime.dwHighDateTime = (DWORD)(info.WriteTime >> 32);
	find.ftLastWriteTime.dwLowDateTime = (DWORD)(info.WriteTime & 0xFFFFFFFF);
	find.ftCreationTime.dwHighDateTime = (DWORD)(info.CreationTime >> 32);
	find.ftCreationTime.dwLowDateTime = (DWORD)(info.CreationTime & 0xFFFFFFFF);
	find.dwFileAttributes = GetAttributesFor(info);
	if((info.Attributes & STAT_FLAG_IS_SYMLINK) != 0) {
		find.dwReserved0 = 0xA000000C; //IO_REPARSE_TAG_SYMLINK;
	}

	LOG(LogInfo, "FindFiles %p found %s", (void*)arg->Info->Context, str.c_str());

	arg->Func(&find, arg->Info);
}

static int DOKAN_CALLBACK DokanFindFiles(
	LPCWSTR			 PathName,
	PFillFindData	fn,	 //call this function with PWIN32_FIND_DATAW
	PDOKAN_FILE_INFO info)    //(see PFillFindData definition)
{
	ProviderDir *dir = GetDirObj(info);
	if(!dir)
		return -ERROR_INVALID_HANDLE;
	FindInfo param;
	param.Func = fn;
	param.Info = info;
	LOG(LogInfo, "FindFiles %p", (void*)info->Context);
	int ret = ErrToWin32Dokan(dir->ListFiles(FindFunc, &param));
	LOG(LogInfo, "FindFiles %p ret %d", (void*)info->Context, ret);
	return ret;
}


static int DOKAN_CALLBACK DokanSetFileAttributes(
	LPCWSTR  FileName,
	DWORD    FileAttributes,
	PDOKAN_FILE_INFO info)
{
	LOG(LogInfo, "SetFileAttr %p", (void*)info->Context);
	return -ERROR_ACCESS_DENIED;
}


static int DOKAN_CALLBACK DokanSetFileTime(
	LPCWSTR		 FileName,
	CONST FILETIME*  CreationTime,
	CONST FILETIME*  LastAccessTime,
	CONST FILETIME*  LastWriteTime,
	PDOKAN_FILE_INFO info)
{
	LOG(LogInfo, "SetFileTime %p", (void*)info->Context);
	return -ERROR_ACCESS_DENIED;
}


// You should not delete file on DeleteFile or DeleteDirectory.
// When DeleteFile or DeleteDirectory, you must check whether
// you can delete the file or not, and return -ERROR_ACCESS_DENIED (when you can delete it)
// or appropriate error codes such as -ERROR_DIR_NOT_EMPTY,
// -ERROR_SHARING_VIOLATION.
// When you return -ERROR_ACCESS_DENIED (ERROR_SUCCESS), you get Cleanup with
// FileInfo->DeleteOnClose set TRUE and you have to delete the
// file in Close.
static int DOKAN_CALLBACK DokanDeleteFile(
	LPCWSTR  FileName,
	PDOKAN_FILE_INFO info)
{
	ProviderCommon *obj = GetCommonObj(info);
	LOG(LogInfo, "DeleteFile %p", (void*)info->Context);
	int err = obj->Delete();
	return ErrToWin32Dokan(err);
}

static int DOKAN_CALLBACK DokanDeleteDirectory( 
	LPCWSTR  FileName,
	PDOKAN_FILE_INFO info)
{
	ProviderCommon *obj = GetCommonObj(info);
	LOG(LogInfo, "DeleteDir %p", (void*)info->Context);
	int err = obj->Delete();
	return ErrToWin32Dokan(err);
}


static int DOKAN_CALLBACK DokanMoveFile(
	LPCWSTR  ExistingFileName,
	LPCWSTR  NewFileName,
	BOOL	 ReplaceExisiting,
	PDOKAN_FILE_INFO info)
{
	std::string oldname = ConvertFilename(ExistingFileName);
	std::string newname = ConvertFilename(NewFileName);
	LOG(LogInfo, "MoveFile %s -> %s (replace: %d)", oldname.c_str(), newname.c_str(), ReplaceExisiting);
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	return fs->Move(oldname, newname, ReplaceExisiting);
}


static int DOKAN_CALLBACK DokanSetEndOfFile(
	LPCWSTR   FileName,
	LONGLONG  Length,
	PDOKAN_FILE_INFO info)
{
	if(info->IsDirectory) {
		LOG(LogError, "SetEndOfFile on directory!");
		return -ERROR_INVALID_HANDLE;
	}
	ProviderFile *f = GetFileObj(info);
	LOG(LogInfo, "SetEndOfFile %p %I64u", f, Length);
	int ret = f->Resize(Length);
	return ErrToWin32Dokan(ret);
}


static int DOKAN_CALLBACK DokanSetAllocationSize(
	LPCWSTR   FileName,
	LONGLONG  Length,
	PDOKAN_FILE_INFO info)
{
	if(info->IsDirectory) {
		LOG(LogError, "SetAllocationSize on directory!");
		return -ERROR_INVALID_HANDLE;
	}
	ProviderFile *f = GetFileObj(info);
	LOG(LogInfo, "SetAllocationSize %p %I64u", f, Length);

	//only actually resize if new size is smaller
	if((unsigned long long)Length < f->Stat().FileSize) {
		int ret = f->Resize(Length);
		return ErrToWin32Dokan(ret);
	}
	return 0;
}


static int DOKAN_CALLBACK DokanLockFile(
	LPCWSTR  FileName,
	LONGLONG  ByteOffset,
	LONGLONG  Length,
	PDOKAN_FILE_INFO info)
{
	if(info->IsDirectory) {
		return -ERROR_INVALID_HANDLE;
	}
	return 0;
}


static int DOKAN_CALLBACK DokanUnlockFile(
	LPCWSTR  FileName,
	LONGLONG ByteOffset,
	LONGLONG Length,
	PDOKAN_FILE_INFO info)
{
	if(info->IsDirectory) {
		return -ERROR_INVALID_HANDLE;
	}
	return 0;
}


// Neither GetDiskFreeSpace nor GetVolumeInformation
// save the DokanFileContext->Context.
// Before these methods are called, CreateFile may not be called.
// (ditto CloseFile and Cleanup)

// see Win32 API GetDiskFreeSpaceEx
static int DOKAN_CALLBACK DokanGetDiskFreeSpace(
	PULONGLONG  FreeBytesAvailable,
	PULONGLONG  TotalNumberOfBytes,
	PULONGLONG  TotalNumberOfFreeBytes,
	PDOKAN_FILE_INFO info)
{
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	LOG(LogInfo, "GetDiskFreeSpace");
	return fs->GetDiskFreeSpace(FreeBytesAvailable, TotalNumberOfBytes, TotalNumberOfFreeBytes);
}


// see Win32 API GetVolumeInformation
static int DOKAN_CALLBACK DokanGetVolumeInformation(
	LPWSTR  VolumeNameBuffer,
	DWORD	 VolumeNameSize, //in num of chars
	LPDWORD VolumeSerialNumber,
	LPDWORD MaximumComponentLength, //in num of chars
	LPDWORD FileSystemFlags,
	LPWSTR	 FileSystemNameBuffer,
	DWORD	 FileSystemNameSize, //in num of chars
	PDOKAN_FILE_INFO info)
{
	LOG(LogInfo, "GetVolumeInformation");
	std::wstring name, type;
	DWORD junk;
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	int ret = fs->GetVolumeInformation(name, type, VolumeSerialNumber ? *VolumeSerialNumber : junk,  MaximumComponentLength ? *MaximumComponentLength : junk, FileSystemFlags ? *FileSystemFlags : junk);
	if(ret)
		return ret;

	if(VolumeNameBuffer && VolumeNameSize > 0) {
		*VolumeNameBuffer = 0;
		if(name.size() > 0) {
			size_t count = name.size();
			if(count > VolumeNameSize-1)
				count = VolumeNameSize-1;
			memcpy(VolumeNameBuffer, &name[0], count * sizeof(WCHAR));
			VolumeNameBuffer[count] = 0;
		}
	}
	if(FileSystemNameBuffer && FileSystemNameSize > 0) {
		*FileSystemNameBuffer = 0;
		if(name.size() > 0) {
			size_t count = type.size();
			if(count > FileSystemNameSize-1)
				count = FileSystemNameSize-1;
			memcpy(FileSystemNameBuffer, &type[0], count * sizeof(WCHAR));
			FileSystemNameBuffer[count] = 0;
		}
	}

	if(VolumeNameBuffer && name.size()+1 >= VolumeNameSize)
		return -ERROR_BUFFER_OVERFLOW;
	if(FileSystemNameBuffer && type.size()+1 >= FileSystemNameSize)
		return -ERROR_BUFFER_OVERFLOW;

	return 0;
}


static int DOKAN_CALLBACK DokanUnmount(
	PDOKAN_FILE_INFO info)
{
	DokanFilesystemMount *fs = ((DokanFilesystemMount*)info->DokanOptions->GlobalContext);
	return 0;
}


// Suported since 0.6.0. You must specify the version at DOKAN_OPTIONS.Version.
static int DOKAN_CALLBACK DokanGetFileSecurity(
	LPCWSTR  FileName,
	PSECURITY_INFORMATION secinfo, //A pointer to SECURITY_INFORMATION value being requested
	PSECURITY_DESCRIPTOR  secdesc, //A pointer to SECURITY_DESCRIPTOR buffer to be filled
	ULONG seclen, //length of Security descriptor buffer
	PULONG  LengthNeeded,
	PDOKAN_FILE_INFO info)
{
	LOG(LogInfo, "GetFileSecurity %p", (void*)info->Context);
	return -ERROR_ACCESS_DENIED;
}

static int DOKAN_CALLBACK DokanSetFileSecurity(
	LPCWSTR  FileName,
	PSECURITY_INFORMATION secinfo,
	PSECURITY_DESCRIPTOR  SecurityDescriptor,
	ULONG  SecurityDescriptorlength,
	PDOKAN_FILE_INFO info)
{
	LOG(LogInfo, "SetFileSecurity %p", (void*)info->Context);
	return -ERROR_ACCESS_DENIED;
}

struct MountInfo
{
	DOKAN_OPTIONS Opts;
	std::wstring Mount;
	DokanFilesystemMount *Fs;
};

DWORD CALLBACK MountThread(void *arg)
{
	DOKAN_OPERATIONS ops = {
		DokanCreateFile,
		DokanOpenDirectory,
		DokanCreateDirectory,
		DokanCleanup,
		DokanCloseFile,
		DokanReadFile,
		DokanWriteFile,
		DokanFlushFileBuffers,
		DokanGetFileInformation,
		DokanFindFiles,
		NULL,
		DokanSetFileAttributes,
		DokanSetFileTime,
		DokanDeleteFile,
		DokanDeleteDirectory,
		DokanMoveFile,
		DokanSetEndOfFile,
		DokanSetAllocationSize,
		DokanLockFile,
		DokanUnlockFile,
		DokanGetDiskFreeSpace,
		DokanGetVolumeInformation,
		DokanUnmount,
		DokanGetFileSecurity,
		DokanSetFileSecurity
	};
	MountInfo *mount = (MountInfo*)arg;

	int ret = DokanMain(&mount->Opts, &ops);
	return ret;
}

MountedFilesystem* FsMount(const char *mount, FilesystemProvider *provider, const Configuration& cfg)
{
	MountInfo *info = new MountInfo();
	info->Mount = Utf8To16(mount);

	memset(&info->Opts, 0, sizeof(DOKAN_OPTIONS));

	DokanFilesystemMount *mnt = new DokanFilesystemMount(provider, info->Mount, cfg);

	info->Opts.Version = DOKAN_VERSION;
	info->Opts.ThreadCount = 6;
	info->Opts.Options = DOKAN_OPTION_KEEP_ALIVE;
	info->Opts.MountPoint = info->Mount.c_str();
	info->Opts.GlobalContext = (ULONG64)mnt;
	info->Fs = mnt;

	DokanRemoveMountPoint(info->Mount.c_str());

	HANDLE h[2];
	h[0] = mnt->GetWaitEvent();
	h[1] = CreateThread(NULL, 0, MountThread, info, 0, NULL);
	DWORD ret = WaitForMultipleObjects(2, h, FALSE, INFINITE);

	if(ret == WAIT_OBJECT_0)
		return mnt;
	delete mnt;
	CloseHandle(h[1]);
	return NULL;
}


void FsInit()
{
}
